%%%-------------------------------------------------------------------
%%% File        : mlib.erl
%%% Author      : Fyodor Ustinov <ufm@ufm.su>
%%% Description : MailEr project
%%%               Some useful functions
%%%
%%% Created : 29 Aug 2009 by Fyodor Ustinov <ufm@ufm.su>
%%%-------------------------------------------------------------------
-module(mlib).

-export([split_header/1,add_kludge/2, add_kludge_to_head/2,
	 genID/0, klusearch/2, add_newkludge/3, bin_to_hex/1,
	 encode_digest_md5/4, decode_digest_md5/1,pad_to/3,
	 match/2]).

%%
%% genID
%%

-spec genID() ->
    string().

genID() ->
    {Ra,Rb,Rc} = now(), 
    Rd = (Ra bsl 52) bor (Rb bsl 24) bor (Rc bsl 4) bor random:uniform(15), 
    lists:flatten(io_lib:format("~.16B",[Rd]))
.



%%
%% add_kludge
%%

-spec add_kludge(Klu :: [], Str :: string()) ->
    {ok, []} | false.

add_kludge(Klu, Str) when is_binary(Str) ->
    add_kludge(Klu,binary_to_list(Str));

add_kludge(Klu, Str)
  when is_list(Klu) ->
    try
	[$ | Ts] = Str,
	Tss =  string:strip(string:strip(string:strip(Ts,both,$\n),both,$\r)),
	K1 = lists:reverse(Klu),
	[A | Tk] = K1,
	{ok,lists:reverse([erlang:append_element(A,Tss) | Tk])}
    catch
	_:_ ->
	    case split_header(Str) of
		{ok, {Kl, Val}} ->
		    {ok, lists:append(Klu,[{Kl, Val}])};
		false  ->
		    false
	    end
    end
.

add_kludge_to_head(Klu, Str)
  when is_list(Klu) ->
    case add_kludge([],Str) of
	{ok, K} ->
	    {ok, [K | Klu]};
	false ->
	    false
    end
.

klusearch(Key,Kl) ->
    L = [X || {X,_Y} <- Kl, string:to_lower(X) =:= string:to_lower(Key)],
    length(L) =/= 0
.

add_newkludge(Kludges,Kl,Val) ->
    Kn = try
	     false = klusearch(Kl,Kludges),
	     {ok, Sn} = mlib:add_kludge(Kludges,Kl ++ ":" ++ Val),
	     Sn
	 catch
	     _:_ -> Kludges
	 end,
    Kn
.

%%
%% split_header
%%

-spec split_header(Value :: string()) -> {ok, {string(), string()}} | false.

split_header(Value) ->
    split_header(Value,[]).


split_header([$: | Value], AccName) ->
    case length(AccName) > 0 of
	true ->
	    {ok,{lists:reverse(AccName),
		 string:strip(string:strip(string:strip(Value,both,$\n),both,$\r))}};
	false ->
	    false
    end;

split_header([Char | Rest], AccName) 
  when Char =:= $- orelse Char =:=$_ ->
    split_header(Rest, [Char | AccName]);

split_header([Char | Rest], AccName) 
  when Char >= $a, Char =< $z ->
    split_header(Rest, [Char | AccName]);

split_header([Char | Rest], AccName) 
  when Char >= $A, Char =< $Z->
    split_header(Rest, [Char | AccName]);

split_header([_Char | _Rest], _AccName) ->
    false.

bin_to_hex(Bin)
  when is_binary(Bin) ->
    bin_to_hex(erlang:binary_to_list(Bin))
;
bin_to_hex(Bin) 
  when is_list(Bin) ->
    lists:flatten([hex(X) || X <- Bin])
.

hex0(L) when L < 10 -> $0 + L;
hex0(L) when L < 16 -> $a - 10 + L.
hex(L) when L < 10 -> [$0, $0+L];
hex(L) when L < 16 -> [$0, $a - 10 + L];
hex(L) when L < 256 -> [hex0(L bsr 4), hex0(L band 16#f)].


encode_digest_md5(Username, Password, AuthWord, D) ->
    {value, {"nonce", Nonce}}     = lists:keysearch("nonce", 1, D),
    {value, {"realm", Realm}}     = lists:keysearch("realm", 1, D),
    case AuthWord of
	"" ->
	    {value, {"cnonce", Cnonce}} = lists:keysearch("cnonce", 1, D),
	    {value, {"digest-uri", Uri}} = lists:keysearch("digest-uri", 1, D);
	_ ->
	    Cnonce = base64:encode_to_string(erlang:md5(genID())),
	    Uri = "smtp/" ++ Realm
%%	    Uri = "imap/" ++ Realm
    end,
    Nc = "00000001",
    Qop = "auth",
%    Uri = "smtp/" ++ Realm ++ "/" ++ Domain,
    A2 = bin_to_hex(erlang:md5(AuthWord ++ ":" ++ Uri)),
    A1 = binary_to_list(erlang:md5(Username ++ ":" ++ Realm ++ ":" ++ Password)) ++
	":" ++ Nonce ++ ":" ++ Cnonce,
    A11 = bin_to_hex(erlang:md5(A1)),
    A22 = Nonce ++ ":" ++ Nc ++ ":" ++ Cnonce ++ ":" ++ Qop ++ ":" ++ A2,
    A = bin_to_hex(erlang:md5(A11 ++ ":" ++ A22)),
    R1 = [{"username", "\"" ++ Username ++ "\""}, 
	  {"cnonce",   "\"" ++ Cnonce ++ "\""},
	  {"nonce", "\"" ++ Nonce ++ "\""},
	  {"nc", Nc},
	  {"qop", Qop},
	  {"digest-uri", "\"" ++ Uri ++ "\""},
	  {"realm", "\"" ++ Realm ++ "\""},
%%	  {"serv-type", "smtp"},
%%	  {"host", Host},
%%	  {"serv-name", Domain},
	  {"response", A}
	 ],
    case lists:keysearch("charset", 1, D) of
	{value, {"charset", Charset}} ->
	    lists:keystore("charset", 1, R1, {"charset", Charset});
	_ ->
	    R1
    end
.

decode_digest_md5(S) ->
    F = fun(X) ->
		case X of
		    [A] ->
			erlang:list_to_tuple(string:tokens(A, "="));
		    [A, B] ->
			{string:strip(A, both, $=), B}
		end
	end,
    [ F(X) || X <- [ string:tokens(X, "\"") || X <- string:tokens(base64:decode_to_string(S),",\n\r")]]
.   

pad_to(T, Fill, Len)
  when length(T) < Len ->
    pad_to(T ++ [Fill], Fill, Len)
;
pad_to(T, _, _) ->
    T
.

%% ----------------------------------------------------------------
%%% Simple pattern matching
%%
%% # - any digit
%% ? - any symbol
%% $ - any character (> ' ')
%% * - any count any characters
%% \ - next character as character
%% match("Test","T*t") -> true
%% match("This is$a test","* $$\\$$ *") -> true
%% match("This isn't a test","* $$ $ *") -> false
%% ----------------------------------------------------------------

match([], []) ->
    true
;

match([], T) ->
    T =:= "*"
;

match(_, "*") ->
    true
;

match(_, []) ->
    false
;

match([Th | Tt] = T, [Mh | Mt]) ->
    case Mh of
	$\\ -> % Next char compare as is
	    [Mhh | Mtt] = Mt,
	    Th =:= Mhh andalso match(Tt, Mtt);

	$# -> % IS DIGIT
	    Th >= $0 andalso Th =< $9 andalso match(Tt, Mt);

	$$ -> % IS Char
	    Th >= $  andalso match(Tt, Mt);

	$? -> % Anything
	    match(Tt, Mt);

	$* ->
	    try_match(T, Mt);
	_ ->
	    Th =:= Mh andalso match(Tt, Mt)
    end
.

try_match([],[]) ->
    true
;

try_match([], M) -> 
    M =:= "*"
;

try_match(_, "*") ->
    true
;

try_match(_, []) ->
    false
;

try_match([_ | Th], M) ->
    case match(Th, M) of
	true ->
	    true;
	false ->
	    try_match(Th, M)
    end
.


